package com.ease.util {

   import com.ease.util.events.DataEvent;
   import com.ease.util.logging.LogManager;

   import flash.display.DisplayObject;
   import flash.events.Event;
   import flash.events.EventDispatcher;
   import flash.utils.Dictionary;

   import mx.core.Application;
   import mx.events.FlexEvent;
   import mx.logging.ILogger;
   import mx.managers.SystemManager;

   [Event(name="{VIEW_SHOW}", type="com.ease.util.events.DataEvent")]
   [Event(name="{VIEW_HIDE}", type="com.ease.util.events.DataEvent")]
   public class ViewVisibilityMonitor extends EventDispatcher {

      // event ids
      public static var VIEW_SHOW:String = "viewShow";
      public static var VIEW_HIDE:String = "viewHide";

      //---------------------------------------------------------------------------
      // Private Fields

      // configuration
      private var _monitorShowEvent:Boolean;
      private var _monitorHideEvent:Boolean;
      private var _useWeakReferences:Boolean;

      // registered views
      private var _views:Set/*DisplayObject*/;

      // view dependencies, for example if we have parent2 -> parent1 -> registeredView:
      //   _dependentViews[parent2]        = registeredView
      //   _dependentViews[parent1]        = registeredView
      //   _dependentViews[registeredView] = registeredView
      // this allows finding a registered view when it or any of its parents changes
      private var _dependentViews:Dictionary/*DisplayObject, Set(DisplayObject)*/;

      // logger
      private static const _logger:ILogger = LogManager.getLogger(ViewVisibilityMonitor);

      //---------------------------------------------------------------------------
      // Constructor

      /**
       * Constructs a new instance.
       *
       * @param monitorShowEvent
       *    Whether to monitor when view becomes visible.
       * @param monitorHideEvent
       *    Whether to monitor when view becomes invisible.
       * @param useWeakReferences
       *    Whether to use weak references to views in internal registries.
       */
      public function ViewVisibilityMonitor(
         monitorShowEvent:Boolean=true,
         monitorHideEvent:Boolean=true,
         useWeakReferences:Boolean=true) {

         _monitorShowEvent = monitorShowEvent;
         _monitorHideEvent = monitorHideEvent;
         _useWeakReferences = useWeakReferences;

         _views = new Set(_useWeakReferences);
         _dependentViews = new Dictionary(_useWeakReferences);
      }

      //---------------------------------------------------------------------------
      // Public Methods

      /**
       * Checks if a given view is currently visible or not. This method checks the
       * <code>visible</code> property of the given view as well as its parents.
       *
       * @param view
       *    The view to check.
       *
       * @return
       *    True, if the view is currently visible, or false otherwise.
       */
      public static function isViewVisible(view:DisplayObject):Boolean {
         // the view is considered invisible if not yet added on the stage
         if (view.stage == null) {
            return false;
         }

         // go from this view through all its parents until the Application is reached
         var currentView:DisplayObject = view;
         while (currentView != null &&
            !(currentView is Application) &&
            !(currentView is SystemManager)) {
            if (!currentView.visible) {
               return false;
            }
            currentView = currentView.parent;
         }

         return true;
      }

      /**
       * Adds a new view to be monitored.
       *
       * @param view
       *    The view to register.
       */
      public function add(view:DisplayObject):void {
         if (!_views.contains(view)) {
            _logger.debug("View add: {0}", view);

            _views.add(view);

            subscribeForStageEvents(view);
            subscribeForVisibilityChange(view);
         }
      }

      /**
       * Removes a view, i.e. stops monitoring for its visibiity changes.
       *
       * @param view
       *    The view to unregister.
       */
      public function remove(view:DisplayObject):void {
         if (_views.contains(view)) {
            _logger.debug("View remove: {0}", view);

            unsubscribeForVisibilityChange(view);
            unsubscribeForStageEvents(view);

            _views.remove(view);
         }
      }

      //---------------------------------------------------------------------------
      // Private Helpers

      /**
       * Subscribes for visibility change events of the given view and its parents.
       */
      private function subscribeForVisibilityChange(view:DisplayObject):void {
         // nothing to do if the view is not on the stage
         if (view.stage == null) {
            return;
         }

         var currentView:DisplayObject = view;

         // go from this view through all its parents until the Application is reached
         while (currentView != null &&
            !(currentView is Application) &&
            !(currentView is SystemManager)) {
            // subscribe for show/hide events only once on this view
            if (_dependentViews[currentView] == null) {
               // subscribe for 'show' events, if needed
               if (_monitorShowEvent) {
                  currentView.addEventListener(FlexEvent.SHOW,
                     onViewVisibilityChange,
                     false,
                     0,
                     true);
               }

               // subscribe for 'hide' events, if needed
               if (_monitorHideEvent) {
                  currentView.addEventListener(FlexEvent.HIDE,
                     onViewVisibilityChange,
                     false,
                     0,
                     true);
               }
            }

            addViewDependency(currentView, view);
            currentView = currentView.parent;
         }
      }

      /**
       * Unsubscribes for visibility change events of the given view and its parents.
       */
      private function unsubscribeForVisibilityChange(view:DisplayObject):void {
         // nothing to do if the view is not on the stage
         if (view.stage == null) {
            return;
         }

         var currentView:DisplayObject = view;

         // go from this view through all its parents until the Application is reached
         while (currentView != null &&
            !(currentView is Application) &&
            !(currentView is SystemManager)) {
            removeViewDependency(currentView, view);

            // unsubscribe for show/hide events on this view if there are no more usages
            if (_dependentViews[currentView] == null) {
               // unsubscribe for 'show' events, if needed
               if (_monitorShowEvent) {
                  currentView.removeEventListener(FlexEvent.SHOW, onViewVisibilityChange);
               }
               // unsubscribe for 'hide' events, if needed
               if (_monitorHideEvent) {
                  currentView.removeEventListener(FlexEvent.HIDE, onViewVisibilityChange);
               }
            }

            currentView = currentView.parent;
         }
      }

      /**
       * Subscribes for events when a view is being added to or removed from the stage.
       */
      private function subscribeForStageEvents(view:DisplayObject):void {
         view.addEventListener(Event.ADDED_TO_STAGE, onStageEvent, false, 0, true);
         view.addEventListener(Event.REMOVED_FROM_STAGE, onStageEvent, false, 0, true);
      }

      /**
       * Unsubscribes for stage events for the given view.
       */
      private function unsubscribeForStageEvents(view:DisplayObject):void {
         view.removeEventListener(Event.ADDED_TO_STAGE, onStageEvent);
         view.removeEventListener(Event.REMOVED_FROM_STAGE, onStageEvent);
      }

      /**
       * Adds a view dependency in terms of _dependentViews[parentView] = view.
       */
      private function addViewDependency(parentView:DisplayObject,
                                         view:DisplayObject):void {
         var dependentViews:Set/*DisplayObject*/ = _dependentViews[parentView] as Set;
         if (dependentViews == null) {
            dependentViews = new Set(_useWeakReferences);
            _dependentViews[parentView] = dependentViews;
         }

         dependentViews.add(view);
      }

      /**
       * Removes a dependency between parent and child views.
       */
      private function removeViewDependency(parentView:DisplayObject,
                                            view:DisplayObject):void {
         var dependentViews:Set/*DisplayObject*/ = _dependentViews[parentView];
         if (dependentViews != null) {
            dependentViews.remove(view);
            if (dependentViews.length == 0) {
               delete _dependentViews[parentView];
            }
         }
      }

      /**
       * Handles a SHOW/HIDE event. As this event may come from a parent of a registered
       * view, first the corresponding registered view is looked for, and then a
       * notification is raised on visibility change.
       */
      private function onViewVisibilityChange(event:FlexEvent):void {
         var changedView:DisplayObject = event.target as DisplayObject;
         _logger.debug(
            "View visibility change ({0}): {1}",
            changedView.toString(),
            event.type);

         // find all registered views that depend on the view changed
         var dependentViews:Set = _dependentViews[changedView] as Set;

         // raise a notification on visibility change for each registered view
         dependentViews.forEach(
            function(view:DisplayObject):void {
               _logger.debug(
                  "   Registered view visibility change: {0}",
                  view.toString());

               // raise a notification for visibility change on the registered view
               fireVisibilityChangeEvent(view, event.type == FlexEvent.SHOW);
            });
      }

      /**
       * Handles ADDED_TO_STAGE/REMOVED_FROM_STAGE events for registered views.
       */
      private function onStageEvent(event:Event):void {
         // identify the view
         var view:DisplayObject = event.target as DisplayObject;
         if (view == null || !_views.contains(view)) {
            Diagnostics.assert(false, "Stage event received for a non-monitored view");
            return;
         }

         _logger.debug("View stage event ({0}): {1}", event.type, view);

         if (event.type == Event.ADDED_TO_STAGE) {
            // subcribe for visibility change events
            subscribeForVisibilityChange(view);

            // if the view is now visible, raise a notification since it has been
            // considered invisible before added to stage
            if (isViewVisible(view) && _monitorShowEvent) {
               fireVisibilityChangeEvent(view, true);
            }
         } else if (event.type == Event.REMOVED_FROM_STAGE) {
            // unsubcribe for visibility change events
            unsubscribeForVisibilityChange(view);

            // if the view is still visible (since the 'stage' property is not null while
            // the view is *about* to be removed from the stage), raise a notification that
            // it is going to become invisible
            if (isViewVisible(view) && _monitorHideEvent) {
               fireVisibilityChangeEvent(view, false);
            }
         }
      }

      /**
       * Dispatches a show/hide event for the given view.
       */
      private function fireVisibilityChangeEvent(view:DisplayObject, visible:Boolean):void {
         dispatchEvent(new DataEvent(visible ? VIEW_SHOW : VIEW_HIDE, view));
      }
   }
}